// golua project golua.go
package lua

import (
	"bytes"
	"fmt"
	"io"
	"math"
	"strconv"
	"strings"
)

const BUF_SIZE = 1024
const EOF = -1
const FIRST_RESERVED = 257

func isNewLine(c int) bool {
	return c == '\n' || c == '\r'
}

func hexValue(c int) int {
	if isDigit(c) {
		return c - '0'
	} else {
		return c | ('A' ^ 'a')
	}
}

func str2int(s string) (iv int, ok bool) {
	v, e := strconv.ParseInt(s, 0, 0)
	if e == nil {
		iv, ok = int(v), true
	}
	return
}

func str2flt(s string) (fv float64, ok bool) {
	neg := false
	if len(s) > 0 && (s[0] == '-' || s[0] == '+') {
		neg = s[0] == '-'
		s = s[1:]
	}
	if strings.HasPrefix(s, "0x") || strings.HasPrefix(s, "0X") { //special format
		intg, frac, fracn, expo, negexpo := 0, 0, 0, 0, 0
		sec := 0 //sec: 0=intg, 1=frac, 2=expo

		if len(s) == 2 {
			goto err_handler
		}

		for i := 2; i < len(s); i++ {
			c := int(s[i])
			if sec < 1 && s[i] == '.' {
				sec = 1
				continue
			}
			if sec < 2 && (c == 'p' || c == 'P') {
				if i == len(s)-1 {
					goto err_handler
				} else if s[i+1] == '-' || s[i+1] == '+' {
					i += 1
					if s[i] == '-' {
						negexpo = 1
					}
					if i == len(s)-1 {
						goto err_handler
					}
					continue
				} else {
					sec = 2
					continue
				}
			}

			if isXDigit(c) {
				switch sec {
				case 0:
					intg = intg*16 + hexValue(c)
				case 1:
					frac = frac*16 + hexValue(c)
					fracn += 1
				case 2:
					if !isDigit(c) {
						goto err_handler
					} else {
						expo = expo*10 + c - '0'
					}
				}
			} else {
				goto err_handler
			}
		}
		fv = (float64(intg) + float64(frac)*math.Pow(16.0, -float64(fracn))) * math.Pow(2.0, float64(negexpo*expo))
		if neg {
			fv = -fv
		}
		ok = true
	} else {
		f, e := strconv.ParseFloat(s, 64)
		if e == nil {
			fv, ok = f, true
			if neg {
				fv = -fv
			}
		}
	}
err_handler:
	return
}

//Token
const (
	/*terminal symbols denoted by reserved word*/
	tk_and = FIRST_RESERVED + iota
	tk_break
	tk_do
	tk_else
	tk_elseif
	tk_end
	tk_false
	tk_for
	tk_function
	tk_goto
	tk_if
	tk_in
	tk_local
	tk_nil
	tk_not
	tk_or
	tk_repeat
	tk_return
	tk_then
	tk_true
	tk_until
	tk_while
	/*other terminal symbols*/
	tk_idiv
	tk_concat
	tk_dots
	tk_eq
	tk_ge
	tk_le
	tk_ne
	tk_shl
	tk_shr
	tk_dbcolon
	TK_EOS
	tk_flt
	tk_int
	tk_name
	tk_string
)

func isResearved(w string) (tt int, ok bool) {
	tt, ok = 0, true
	switch w {
	case "and":
		tt = tk_and
	case "break":
		tt = tk_break
	case "do":
		tt = tk_do
	case "else":
		tt = tk_else
	case "elseif":
		tt = tk_elseif
	case "end":
		tt = tk_end
	case "false":
		tt = tk_false
	case "for":
		tt = tk_for
	case "function":
		tt = tk_function
	case "goto":
		tt = tk_goto
	case "if":
		tt = tk_if
	case "in":
		tt = tk_in
	case "local":
		tt = tk_local
	case "nil":
		tt = tk_nil
	case "not":
		tt = tk_not
	case "or":
		tt = tk_or
	case "repeat":
		tt = tk_repeat
	case "return":
		tt = tk_return
	case "then":
		tt = tk_then
	case "true":
		tt = tk_true
	case "until":
		tt = tk_until
	case "while":
		tt = tk_while
	default:
		ok = false
	}
	return
}

type Token struct {
	T       int
	Seminfo interface{}
}

func txtToken(t int) string {
	switch t {
	case tk_and:
		return "and"
	case tk_break:
		return "break"
	case tk_do:
		return "do"
	case tk_else:
		return "else"
	case tk_elseif:
		return "elseif"
	case tk_end:
		return "end"
	case tk_false:
		return "false"
	case tk_for:
		return "for"
	case tk_function:
		return "function"
	case tk_goto:
		return "goto"
	case tk_if:
		return "if"
	case tk_in:
		return "in"
	case tk_local:
		return "local"
	case tk_nil:
		return "nil"
	case tk_not:
		return "not"
	case tk_or:
		return "or"
	case tk_repeat:
		return "repeat"
	case tk_return:
		return "return"
	case tk_then:
		return "then"
	case tk_true:
		return "true"
	case tk_until:
		return "until"
	case tk_while:
		return "while"
	case tk_idiv:
		return "//"
	case tk_concat:
		return ".."
	case tk_dots:
		return "..."
	case tk_eq:
		return "=="
	case tk_ge:
		return ">="
	case tk_le:
		return "<="
	case tk_ne:
		return "~="
	case tk_shl:
		return "<<"
	case tk_shr:
		return ">>"
	case tk_dbcolon:
		return "::"
	case TK_EOS:
		return "EOS"
	case tk_flt:
		return "float"
	case tk_int:
		return "int"
	case tk_name:
		return "name"
	case tk_string:
		return "string"
	default:
		return fmt.Sprintf("\"%c\"", t)
	}
}

func (t *Token) String() string {
	switch t.T {
	case tk_flt, tk_int, tk_name:
		return fmt.Sprintf("<%s:%v>", txtToken(t.T), t.Seminfo)
	case tk_string:
		return fmt.Sprintf("<%s:%s", txtToken(t.T), strconv.Quote(t.Seminfo.(string)))
	default:
		return fmt.Sprintf("<%s>", txtToken(t.T))
	}
}

//golua lexer
type Lexer struct {
	reader  io.ByteReader
	line    int
	current int
	buff    *bytes.Buffer
}

func NewLexer(reader io.ByteReader) *Lexer {
	l := new(Lexer)
	l.reader = reader
	l.line = 1
	l.buff = bytes.NewBuffer(make([]byte, BUF_SIZE))

	l.getc()
	return l
}

func (l *Lexer) lexerror(msg string) {
	msg = fmt.Sprintf("[lex-error]%d:%s", l.line, msg)
	panic(msg)
}

//func (l *lexer) txtToken(token int) string {
//	switch token {
//	case tk_name, tk_string, tk_flt, tk_int:
//		return fmt.Sprintf("'%s'", l.buff.String())

//	default:
//		return strTokens[token-FIRST_RESERVED]
//	}
//}
func (l *Lexer) readNumeral(leadDot bool) (tk int, iv int, fv float64) {
	l.buff.Reset()
	if leadDot {
		l.buff.WriteByte('.')
	}
	expo := "eE"
	if l.checkNext2("00") {
		if l.checkNext2("xX") {
			expo = "pP"
		}
	}
	for {
		if l.checkNext2(expo) {
			l.checkNext2("-+")
		}
		if isXDigit(l.current) || l.current == '.' {
			l.saveAndNext()
		} else {
			break
		}
	}
	s := l.getString()
	i, ok := str2int(s)
	if ok {
		tk, iv = tk_int, i
		return
	}
	f, ok := str2flt(s)
	if ok {
		tk, fv = tk_flt, f
		return
	}
	panic(fmt.Sprintf("malformed numeral: %s", s))
}

func (l *Lexer) readString() string {
	del := l.current
	l.getc() //skip delimiter

	l.buff.Reset()
	for l.current != del {
		switch l.current {
		case EOF:
			l.lexerror("unfinished string")
		case '\n', '\r':
			l.lexerror("unfinished string")
		case '\\': //escape sequence
			l.saveAndNext()
			c := -1
			switch l.current {
			case 'a':
				c = '\a'
			case 'b':
				c = '\b'
			case 'f':
				c = '\f'
			case 'n':
				c = '\n'
			case 'r':
				c = '\r'
			case 't':
				c = '\t'
			case 'v':
				c = '\v'
			case '\n', '\r':
				c = '\n'
				l.incLineNumber()
			case '"', '\'', '\\':
				c = l.current
				//TODO: add \x and \u
			}

			if c >= 0 { //escape success
				l.buff.Truncate(l.buff.Len() - 1)
				l.buff.WriteByte(byte(c))
				l.getc() //skip
			} else { //failed escape
				l.saveAndNext()
			}

			//			read_save:
			//				l.getc()
			//			only_save:
			//				l.buff.Truncate(l.buff.Len() - 1)
			//				l.buff.WriteByte(byte(c))
			//			no_save:
		default:
			l.saveAndNext()
		}
	}
	l.getc() //skip delimiter
	return l.getString()
}

func (l *Lexer) readLongString(sep int, ignore bool) string {
	l.buff.Reset()
	line := l.line
	l.getc() //skip the 2nd '['
	if isNewLine(l.current) {
		l.incLineNumber() //skip the start newline
	}
	for {
		switch l.current {
		case EOF:
			what := "string"
			if ignore {
				what = "comment"
			}
			l.lexerror(fmt.Sprintf("unfinished long %s (starting at line %d)", what, line))
		case '\n', '\r':
			if !ignore {
				l.buff.WriteByte('\n')
			}
			l.incLineNumber()
			if ignore {
				l.buff.Reset() //do not waste memory
			}
		case ']':
			if l.skipSep() == sep {
				l.saveAndNext() //skip 2nd ']'
				goto endloop
			}
		default:
			if !ignore {
				l.saveAndNext()
			} else {
				l.getc()
			}
		}
	}
endloop:
	if !ignore {
		l.buff.Truncate(l.buff.Len() - sep - 2) //now the buffer is the real string
	}
	return l.getString()
}

func (l *Lexer) incLineNumber() {
	old := l.current
	l.getc()
	if isNewLine(l.current) && l.current != old {
		l.getc()
	}
	l.line += 1
}

func (l *Lexer) getc() {
	c, e := l.reader.ReadByte()
	if e != nil {
		l.current = EOF
	} else {
		l.current = int(c)
	}
}

func (l *Lexer) getString() string {
	ret := l.buff.String()
	l.buff.Reset()
	return ret
}

func (l *Lexer) checkNext1(c int) bool {
	if l.current == c {
		l.getc()
		return true
	}
	return false
}

func (l *Lexer) checkNext2(s string) bool {
	if l.current == int(s[0]) || l.current == int(s[1]) {
		l.saveAndNext()
		return true
	}
	return false
}

func (l *Lexer) saveAndNext() {
	l.buff.WriteByte(byte(l.current))
	l.getc()
}

func (l *Lexer) skipSep() int {
	count, s := 0, l.current
	l.saveAndNext() //skip first '['
	for l.current == '=' {
		count += 1
		l.saveAndNext()
	}
	if s != l.current {
		count = -count - 1
	}
	return count
}

func (l *Lexer) NextTok(seminfo *interface{}) int {
	for {
		switch l.current {
		case EOF:
			return TK_EOS
		case '\n', '\r':
			l.incLineNumber()
		case ' ', '\f', '\t', '\v': //skip space
			l.getc()
		case '-': //'-' or comment
			l.getc()
			if l.current != '-' {
				return '-'
			}
			l.getc()
			if l.current == '[' { //long comment
				sep := l.skipSep()
				if sep >= 0 {
					l.readLongString(sep, true)
					break
				}
			}
			//else short comment
			for !isNewLine(l.current) && l.current != EOF {
				l.getc()
			}
		case '[': //long string or simple '['
			var line = l.line
			sep := l.skipSep()
			if sep >= 0 {
				*seminfo = l.readLongString(sep, false)
				return tk_string
			} else if sep == -1 {
				return '['
			} else {
				l.lexerror(fmt.Sprintf("invalid long string delimeter (starting at %d)", line))
			}
		case '=':
			l.getc()
			if l.checkNext1('=') {
				return tk_eq
			} else {
				return '='
			}
		case '<':
			l.getc()
			if l.checkNext1('=') {
				return tk_le
			} else if l.checkNext1('<') {
				return tk_shl
			} else {
				return '<'
			}
		case '>':
			l.getc()
			if l.checkNext1('=') {
				return tk_ge
			} else if l.checkNext1('>') {
				return tk_shr
			} else {
				return '>'
			}
		case '/':
			l.getc()
			if l.checkNext1('/') {
				return tk_idiv
			} else {
				return '/'
			}
		case '~':
			l.getc()
			if l.checkNext1('=') {
				return tk_ne
			} else {
				return '~'
			}
		case ':':
			l.getc()
			if l.checkNext1(':') {
				return tk_dbcolon
			} else {
				return ':'
			}
		case '"', '\'': //short literal string
			*seminfo = l.readString()
			return tk_string
		case '.': //'.' '..' '...' or number
			l.getc()
			if l.checkNext1('.') {
				if l.checkNext1('.') {
					return tk_dots
				} else {
					return tk_concat
				}
			} else if isDigit(l.current) {
				tk, iv, fv := l.readNumeral(true)
				if tk == tk_int {
					*seminfo = iv
				} else {
					*seminfo = fv
				}
				return tk
			} else {
				return '.'
			}
		case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': //number
			tk, iv, fv := l.readNumeral(false)
			if tk == tk_int {
				*seminfo = iv
			} else {
				*seminfo = fv
			}
			return tk
		default: //identifier or reserved word //single char token
			if isAlpha(l.current) {
				l.buff.Reset()
				for isAlnum(l.current) {
					l.saveAndNext()
				}
				word := l.getString()
				*seminfo = word
				if t, ok := isResearved(word); ok {
					return t
				} else {
					return tk_name
				}
			} else { //single char token
				c := l.current
				l.getc()
				return c
			}
		}
	}
}
